Skip to main content

Cấu hình Search Plugin

10. Quy trình thêm search cho một bảng mới

Bước 1: Xác định repo và file cần sửa

Backend search plugin đang nằm trong repo:

k12-tunnel-v5

Khi thêm search cho bảng mới, sửa đúng file config theo module:

Module GraphQLService nameFile config
Student services, thư viện, y tế, nội trú, bus, foodcore_studentsrc/plugins/search-plugin/configs/library-search-configs.js hoặc tạo file config riêng nếu module mới đủ lớn
Academic, LMScore_academicsrc/plugins/search-plugin/configs/lms-search-configs.js
User/profile dùng trong service studentcore_studentsrc/plugins/search-plugin/configs/user-search-configs.js

Thư mục chứa các file config search:

src/plugins/search-plugin/configs/

File gom toàn bộ config vào PostGraphile plugin:

src/plugins/search-plugin/create-plugin.js

Nếu tạo thêm file config mới, phải import và spread file đó trong SEARCH_PLUGIN_CONFIGS của create-plugin.js.

File cơ chế sinh SQL search:

src/plugins/search-plugin/search-plugin-fatory.js

Không sửa file này khi chỉ thêm search cho bảng mới. Chỉ sửa khi cần đổi behavior chung của search plugin.

Nếu app đang chạy bằng schema đã export, file generated nằm ở:

src/generated/<module>_schema.mjs

Sau khi sửa config backend, cần export lại schema cho đúng module để file generated nhận field search mới.

Bước 2: Xác định đúng service, schema và table

Không suy đoán từ tên GraphQL. Dùng tên vật lý trong PostgreSQL:

{
serviceName: "core_student",
schemaName: "public",
tableName: "library_example",
}

Quy tắc:

  • serviceName phải khớp module trong src/modules.config.mjs.
  • schemaName mặc định là public; chỉ bỏ qua khi bảng thật sự nằm trong public.
  • tableName là tên bảng PostgreSQL dạng snake_case, không phải tên GraphQL collection.
  • Bảng muốn search phải được expose trong module tương ứng của src/modules.config.mjs; nếu bảng bị filter khỏi module thì config search không có tác dụng với schema đó.

Ví dụ module hiện có:

core_student  -> /graphql/core-student
core_academic -> /graphql/core-academic
core_shared -> /graphql/core-shared

Bước 3: Chọn cột search trên bảng chính

Chỉ thêm các cột text mà người dùng thật sự cần tìm:

columnNames: ["code", "name", "description"];

Không dùng tên GraphQL camelCase:

// Sai
columnNames: ["fullName"];

// Đúng
columnNames: ["full_name"];

Có thể để rỗng nếu bảng chính không search trực tiếp và chỉ search qua relation:

columnNames: [];

Nếu không khai báo columnNames, plugin mặc định dùng:

columnNames: ["name"];

Chỉ bỏ qua columnNames khi chắc chắn bảng có cột name.

Bước 4: Khai báo relationSearches nếu cần search qua bảng liên quan

Với mỗi relation, xác định rõ:

  • Bảng cha hiện tại.
  • Bảng liên quan.
  • Schema của bảng liên quan nếu khác schema cha.
  • Cột join ở bảng cha.
  • Cột join tương ứng ở bảng liên quan.
  • Có cần school_id hoặc khóa tenant khác hay không.

Nên viết điều kiện SQL trước:

parent.foreign_id = relation.id
AND parent.school_id = relation.school_id

Sau đó chuyển thành config:

relationSearches: [
{
tableName: "library_subject",
localColumns: ["subject_id", "school_id"],
remoteColumns: ["id", "school_id"],
columnNames: ["name"],
},
];

Với hệ thống nhiều trường, không bỏ school_id nếu relation phụ thuộc tenant. Join thiếu tenant có thể trả dữ liệu sai trường.

Relation khác schema phải khai báo schemaName trên relation:

{
schemaName: "library",
serviceName: "core_student",
tableName: "library_borrow_book_report",
columnNames: [],
relationSearches: [
{
schemaName: "public",
tableName: "library_book",
localColumns: ["book_id", "school_id"],
remoteColumns: ["id", "school_id"],
columnNames: ["title"],
},
],
}

Relation lồng nhau được tính từ bảng cha trực tiếp, không tính từ bảng gốc:

{
serviceName: "core_student",
tableName: "library_dang_ki_ca_biet",
columnNames: ["code"],
relationSearches: [
{
tableName: "library_book_copy",
localColumns: ["book_copy_id", "school_id"],
remoteColumns: ["id", "school_id"],
columnNames: ["isbn"],
relationSearches: [
{
tableName: "library_book",
localColumns: ["book_id", "school_id"],
remoteColumns: ["id", "school_id"],
columnNames: ["title"],
},
],
},
],
}

Bước 5: Thêm config vào đúng file hoặc tạo file config mới

Ví dụ thêm search cho bảng thuộc core_student:

// src/plugins/search-plugin/configs/library-search-configs.js
const LIBRARY_SEARCH_PLUGIN_CONFIGS = [
{
serviceName: "core_student",
tableName: "library_example",
columnNames: ["code", "name"],
relationSearches: [
{
tableName: "library_subject",
localColumns: ["subject_id", "school_id"],
remoteColumns: ["id", "school_id"],
columnNames: ["name"],
},
],
},
];

Nếu module đã có file config phù hợp, thêm object mới vào mảng config hiện có. Ví dụ:

  • Bảng thư viện hoặc student service: thêm vào src/plugins/search-plugin/configs/library-search-configs.js.
  • Bảng LMS/academic: thêm vào src/plugins/search-plugin/configs/lms-search-configs.js.
  • Bảng user/profile đang phục vụ service student: thêm vào src/plugins/search-plugin/configs/user-search-configs.js.

Nếu module mới đủ lớn hoặc không thuộc các nhóm trên, tạo file config mới trong:

src/plugins/search-plugin/configs/

Đặt tên theo module/nghiệp vụ:

<module>-search-configs.js

Ví dụ tạo file:

src/plugins/search-plugin/configs/medical-search-configs.js

Nội dung file mới phải export default một mảng config:

const MEDICAL_SEARCH_PLUGIN_CONFIGS = [
{
serviceName: "core_student",
tableName: "medical_student_health_records",
columnNames: ["code", "full_name"],
},
];

export default MEDICAL_SEARCH_PLUGIN_CONFIGS;

Sau đó đăng ký file mới trong:

src/plugins/search-plugin/create-plugin.js

Ví dụ:

import MEDICAL_SEARCH_PLUGIN_CONFIGS from "./configs/medical-search-configs.js";

const SEARCH_PLUGIN_CONFIGS = [
...LIBRARY_SEARCH_PLUGIN_CONFIGS,
...LMS_SEARCH_PLUGIN_CONFIGS,
...USER_SEARCH_PLUGIN_CONFIGS,
...MEDICAL_SEARCH_PLUGIN_CONFIGS,
];

Nếu quên import và spread vào SEARCH_PLUGIN_CONFIGS, file config mới sẽ không được PostGraphile nạp và GraphQL condition sẽ không có field search cho bảng đó.

Checklist config bắt buộc:

  • serviceName đúng module GraphQL.
  • schemaName đúng schema database nếu không phải public.
  • tableName đúng tên bảng PostgreSQL.
  • columnNames dùng tên cột PostgreSQL.
  • Mỗi relation có tableName.
  • localColumns.length === remoteColumns.length.
  • Thứ tự localColumnsremoteColumns khớp nhau.
  • Relation có columnNames hoặc relationSearches con.
  • Relation khác schema đã khai báo schemaName.

Search plugin dùng ILIKE, pg_trgmpublic.immutable_unaccent. Khi thêm cột search mới, cần thêm migration tạo index phù hợp trong repo migration/database của hệ thống đang quản lý schema đó.

Index nên tạo trên chính bảng chứa cột được search. Nếu cột nằm ở relation, tạo index trên bảng relation, không tạo trên bảng chính.

Pattern index cho search không phân biệt dấu:

CREATE INDEX IF NOT EXISTS <table_name>_<column_name>_trgm_idx
ON <schema_name>.<table_name>
USING gin (
public.immutable_unaccent(coalesce(<column_name>, '')) gin_trgm_ops
);

Ví dụ:

CREATE INDEX IF NOT EXISTS library_book_title_trgm_idx
ON public.library_book
USING gin (
public.immutable_unaccent(coalesce(title, '')) gin_trgm_ops
);

Database cũng phải có:

CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS unaccent;

và function:

public.immutable_unaccent(text)

Nếu thiếu function này, query có condition.search sẽ lỗi khi PostgreSQL thực thi SQL.

Bước 7: Export lại schema hoặc restart Graphile

Nếu chạy dev trực tiếp bằng config source, restart/reload tiến trình Graphile để plugin được nạp lại.

Nếu chạy qua src/server-multi.mjs, runtime đọc schema đã export trong:

src/generated/<module>_schema.mjs

Khi đó cần export lại đúng module sau khi sửa config. Ví dụ với core_student:

TARGET_MODULE=core_student yarn export

Lệnh export cần DATABASE_URL trỏ đúng database. Không sửa tay file generated trừ khi chỉ kiểm tra tạm thời; nguồn đúng vẫn là config trong src/plugins/search-plugin/configs/*.js.

Bước 8: Cập nhật dashboard/client query

Search phải đi qua condition.search, không đưa keyword vào filter.includesInsensitive nếu bảng đã dùng plugin này.

Ví dụ query search cho PHXTableV6:

export const searchExampleV6 = (params: Params) => ({
enable: true,
keyResult: "allCoreStudentPublicLibraryExamples",
query: `
query searchExampleV6 {
allCoreStudentPublicLibraryExamples(
condition: {
schoolId: ${params.schoolId}
search: "%@value%"
}
filter: {
deletedAt: {isNull: true}
}
first: ${ROW_NUMBER.DEFAULT}
last: ${ROW_NUMBER.DEFAULT}
after: ""
before: ""
) {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
nodes {
rowId
name
}
totalCount
}
}
`,
});

Khi gắn vào table:

<PHXTableV6
list={listExampleV6(params)}
search={searchExampleV6(params)}
{...otherProps}
/>

Yêu cầu với query dashboard:

  • enable: true trong object search.
  • keyResult đúng root collection.
  • Dùng placeholder search: "%@value%" theo convention hiện tại của PHXTableV6.
  • Giữ các filter nghiệp vụ như schoolId, warehouseId, deletedAt, trạng thái, khoảng ngày.
  • Query list và search trả về cùng shape: pageInfo, nodes, totalCount và cùng field trong nodes.

Bước 9: Kiểm tra GraphQL condition

Introspect condition type để chắc field search đã xuất hiện:

query CheckSearchCondition {
__type(name: "CoreStudentPublicLibraryExampleCondition") {
inputFields {
name
}
}
}

Kết quả cần có:

{
"name": "search"
}

Nếu chưa có search, kiểm tra lại:

  • Đã sửa đúng config file chưa.
  • serviceName, schemaName, tableName có đúng không.
  • Bảng có nằm trong module của src/modules.config.mjs không.
  • Đã restart Graphile hoặc export lại schema generated chưa.
  • Client có gọi đúng endpoint module không.

Tối thiểu kiểm tra các trường hợp sau:

  • Keyword khớp cột trực tiếp.
  • Keyword khớp cột relation.
  • Keyword khớp relation lồng nhau nếu có.
  • Keyword không dấu khớp dữ liệu có dấu, ví dụ nguoi khớp người.
  • Keyword có dấu nhưng khác dấu vẫn được normalize, ví dụ ngươi khớp người theo cơ chế search không phân biệt dấu hiện tại.
  • Keyword khác hoa/thường vẫn khớp.
  • Keyword nhiều từ chỉ khớp khi cùng một cột hoặc cùng một nhánh relation chứa đủ các từ.
  • Keyword không tồn tại trả về totalCount: 0.
  • Filter nghiệp vụ vẫn đúng, không trả dữ liệu sai trường, sai kho hoặc bản ghi đã xóa.
  • Pagination vẫn hoạt động sau khi search.

Logic search hiện tại:

  • Tách keyword bằng khoảng trắng.
  • Bỏ dấu cả keyword và dữ liệu trước khi so sánh.
  • Dùng ILIKE để không phân biệt hoa/thường.
  • Các cột và relation nối với nhau bằng OR.
  • Trong một cột hoặc một nhánh relation, mọi từ trong keyword phải cùng khớp.
  • Fuzzy matching dùng pg_trgm và giới hạn số từ được sửa bằng cấu hình trong createTextSearchPlugin.

Bước 11: Checklist trước khi merge

  • Đã thêm config vào đúng file trong src/plugins/search-plugin/configs/.
  • Nếu tạo config file mới, đã import trong src/plugins/search-plugin/create-plugin.js.
  • Bảng thuộc đúng module trong src/modules.config.mjs.
  • Đã thêm migration index cho từng cột search mới.
  • Đã export lại src/generated/<module>_schema.mjs nếu runtime dùng schema generated.
  • Dashboard query dùng condition.search.
  • PHXTableV6 có object search với enable: true.
  • List và search query có cùng shape dữ liệu.
  • Đã kiểm tra search có dấu, không dấu, nhiều từ và relation.